//
// Created by 86134 on 2024/1/18.
//

#include "Socket.hpp"
#include "../base/Defind.hpp"
#include "../base/BaseLog.hpp"

#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static int BindAndListen(std::string& ip, int port, int fd) {
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    socklen_t len = sizeof(addr);

    int ret = 0;
    if ((ret = bind(fd, (struct sockaddr *)&addr, len)))
    {
        if (errno == EACCES) 
        {
            error("permission is denide. (err:%d)\n", errno);
        }
        if (errno == EINVAL)
        {
            error("The socket is already bound to an address.(err:%d)\n", errno);
        }
        if (errno == EROFS)
        {
            error("The socket inode would reside on a ready-only filesystem.(err:%d)\n", errno);
        }
        return -1;
    }
    
    if ((ret = listen(fd, BACKLOG)) == -1)
    {
        if (errno == EADDRINUSE)
        {
            error("Another socket is already listening on the same port.(err:%d)\n", errno);
        }
        return -1;
    }
    return fd;
}

static int Socket_() {
    int fd;
    if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        error("open a socket fd fail.(err:%d)\n", errno);
        return -1;
    }
    return fd;
}

static int Accept(int listen_fd, std::string& ip, int& port) {
    struct sockaddr_in cli_addr;
    socklen_t len = sizeof(cli_addr);
    int cli_fd;
    if ((cli_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &len)) == -1)
    {
        if (errno == ECONNABORTED)
        {
            error("The conneciton has been aborted.(err:%d)\n", errno);
        }
        if (errno == ENFILE)
        {
            error("The system-wide limit on the total number of open files has been reached.(err:%d)\n", errno);
        }
        if (errno == ENOBUFS || errno == ENOMEM) 
        {
            error("Not enough free memory.(err:%d)\n", errno);
        }
        return -1;
    }
    else
    {
        info("client(%d)\n", cli_fd);
    }
    port = ntohs(cli_addr.sin_port);
    char *_ip = inet_ntoa(cli_addr.sin_addr);
    ip = std::string(_ip);
    return cli_fd;
}

static int Connect(std::string& ip, int port, int fd) {
    struct sockaddr_in ser_addr;
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(port);
    ser_addr.sin_addr.s_addr = inet_addr(ip.c_str());
    socklen_t len = sizeof(ser_addr);

    if (connect(fd, (struct sockaddr *)&ser_addr, len)) {
        if (errno == ECONNREFUSED)
        {
            error(" no server listening on %s:%d\n", ip.c_str(), port);
        } else if (errno == EALREADY)
        {
            error("the socket is unblocking and there is already has a connetion on %s:%d\n", ip.c_str(), port);
        } else {
            error("the address is inavail!\n");
        }
        return -1;
    }
    return 0;
}

static bool GetBlock(int fd) {
    int old_mask;
    // get the mask 
    if ((old_mask = fcntl(fd, F_GETFL, 0)) < -1)
    {
        error("fcntl get() fail! (errno:%d)\n", errno);
        return -1;
    }
    // check the mask
    if (old_mask|O_NONBLOCK) return false;
    else return true;
}

static int Fcntl(int fd, int mask, bool is_clean) {
    int old_mask;

    if ((old_mask = fcntl(fd, F_GETFL, 0)) == -1)
    {
        error("GETFL fail!(%d)\n", errno);
        return -1;
    }

    // clean the given mask
    if (is_clean)
    {
        if (fcntl(fd, F_SETFL, old_mask|(~mask)) == -1)
        {
            error("SETFL fail!(%d)\n", errno);
            return -1;
        }
        else return 0;
    }
    
    // set the given mask
    if (fcntl(fd, F_SETFL, mask|old_mask) == -1)
    {
        error("SETFL fail!(%d)\n", errno);
        return -1;
    }
    return 0;
}

static int SetNonBlock(int fd) {
    return Fcntl(fd, O_NONBLOCK, false);
}

[[maybe_unused]] static int SetBlock(int fd) {
    return Fcntl(fd, O_NONBLOCK, true);
}

static bool GetReuseAddr(int fd) {
    int yes = 0;
    socklen_t len = sizeof(yes);
    if (getsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, &len) == -1) {
        error("getsockeopt() fail! (err : %d)\n", errno);
        return -1;
    }
    if (yes) return true;
    else return false;
}

static int SetReuseAddr(int fd) {
    int yes = 1;
    /* Make sure connection-intensive things like the redis benchmark
     * will be able to close/open sockets a zillion of times */
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1) {
        error("setsocketopt() fail! (err : %d)\n", errno);
        return -1;
    }
    return  0;;
}



/*------------------------------------------
 * 
 * Socket Api
 * 
 * -----------------------------------------
*/
int
DBServer::Socket::socket_client(Connection& conn) {
    int fd = -1;
    if ((fd = Socket_()) == -1)
    {
        return -1;    
    }
    conn.set_socket_fd(fd);

    if (Connect(conn.get_socket_info()->ip, 
            conn.get_socket_info()->port, 
            conn.get_socket_fd()) == -1)
    {
        conn.set_socket_status(OPENING);
        return -1;
    }
    conn.set_socket_status(CONNECTED);
    return 1;
}

int
DBServer::Socket::socket_server(Connection& conn) {
    int listen_fd = -1;
    if ((listen_fd = Socket_()) == -1)
    {
        return -1;
    }
    conn.set_socket_fd(listen_fd);
    // bind and listen
    int ret = BindAndListen(conn.get_socket_info()->ip, 
                                conn.get_socket_info()->port, listen_fd);
    if (ret == -1)
    {
        close(listen_fd);
        conn.set_socket_fd(-1);
        return -1;
    }
    conn.set_socket_status(LISTENING);
    return 0;
}

int
DBServer::Socket::socket_accept(Connection& conn, int listen_fd) {
    // connecion ip and port
    int port;
    std::string ip;
    // accpet
    int new_fd = Accept(listen_fd, ip, port);
    if (new_fd == -1)
    {
        return -1;
    }
    // set the connection's attributions
    conn.set_socket_fd(new_fd);
    conn.set_socket_status(CONNECTED);
    conn.set_socket_info(std::move(ip), port);
    return  0;
}

int 
DBServer::Socket::socket_close(Connection& conn) {
    close(conn.get_socket_fd());
    return 1;
} 

int 
DBServer::Socket::socket_set_unblock(Connection& conn) {
    return SetNonBlock(conn.get_socket_fd());
}

int 
DBServer::Socket::socket_set_reuse(Connection& conn) {
    return SetReuseAddr(conn.get_socket_fd());
}

bool 
DBServer::Socket::socket_get_unblock(Connection& conn) {
    return GetBlock(conn.get_socket_fd());
}

bool 
DBServer::Socket::socket_get_reuse(Connection& conn) {
    return GetReuseAddr(conn.get_socket_fd());
}